﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Noris.Tools.TraceDownloader.Downloader
{
    #region PackMode : režimy zipování
    /// <summary>
    /// PackMode : režimy zipování
    /// </summary>
    internal enum PackMode
    {
        None = 0,
        IonicBestSpeed,
        IonicBestCompession,
        WinCabMin,
        IonicBZip2,
        WinCabMax,
        SevenZipSpeed,
        SevenZipCompression
    }
    #endregion
    #region PackInfo : Třída popisující režimy pakování, jejich rychlosti a kompresní poměry. Abstraktní předek pro konkrétní implementace pakování.
    /// <summary>
    /// PackInfo : Třída popisující režimy pakování, jejich rychlosti a kompresní poměry.
    /// Abstraktní předek pro konkrétní implementace pakování.
    /// </summary>
    internal abstract class PackInfo
    {
        #region Konstrukce a základní datové property
        /// <summary>
        /// Konstruktor je privátní, objekty se generují statickou metodou Create() nebo přes property ModeList.
        /// </summary>
        /// <param name="packMode"></param>
        /// <param name="speedMBs"></param>
        /// <param name="compressionRatio"></param>
        /// <param name="multiThreadSupport"></param>
        /// <param name="toolTip"></param>
        protected PackInfo(PackMode packMode, decimal speedMBs, decimal compressionRatio, bool multiThreadSupport, string toolTip)
        {
            this.PackMode = packMode;
            this.PresetSpeedMBs = speedMBs;
            this.PresetCompressionRatio = compressionRatio;
            this.MultiThreadSupport = multiThreadSupport;
            this.ToolTip = toolTip;
            this.WorkState = AsyncWorkState.None;
        }
        public override string ToString()
        {
            return this.GetType().Name +
                "; Speed: " + this.SpeedText +
                "; Compression: " + this.CompressionFraction;
        }
        /// <summary>
        /// Obsahuje (vždy vytvoří nový) soupis režimů a jejich vizuální vlastnosti
        /// </summary>
        internal static IEnumerable<PackInfo> ModeList
        {
            get
            {
                List<PackInfo> result = new List<PackInfo>();
                result.Add(Create(PackMode.IonicBestSpeed));
                result.Add(Create(PackMode.IonicBestCompession));
                result.Add(Create(PackMode.IonicBZip2));
                result.Add(Create(PackMode.WinCabMin));
                result.Add(Create(PackMode.WinCabMax));
                result.Add(Create(PackMode.SevenZipSpeed));
                result.Add(Create(PackMode.SevenZipCompression));
                return result;
            }
        }
        /// <summary>
        /// Vrátí objekt daného typu (class factory)
        /// </summary>
        /// <param name="packMode"></param>
        /// <returns></returns>
        internal static PackInfo Create(PackMode packMode)
        {
            if (PackIonicInfo.ImplementPackMode(packMode)) return PackIonicInfo.Create(packMode);
            if (PackWinCabInfo.ImplementPackMode(packMode)) return PackWinCabInfo.Create(packMode);
            if (Pack7zDllInfo.ImplementPackMode(packMode)) return Pack7zDllInfo.Create(packMode);
            throw new InvalidOperationException("Třída PackInfo nemůže vygenerovat objekt pro režim PackMode = " + packMode.ToString() + ", nezná třídu která by jej implementovala.");
        }
        public PackMode PackMode { get; protected set; }
        /// <summary>
        /// Rychlost v MB/sec.
        /// Hodnota přednastavená z programu.
        /// </summary>
        public decimal PresetSpeedMBs { get; protected set; }
        /// <summary>
        /// Rychlost v MB/sec.
        /// Hodnota ověřená praxí, aktuálně načtená z PackPerformance.
        /// </summary>
        public decimal SpeedMBs { get { return PackPerformance.GetSpeedMBs(this.PackMode); } }
        /// <summary>
        /// Text popisující rychlost
        /// </summary>
        public string SpeedText { get { return this.SpeedMBs.ToString("# ##0.0").Trim() + " MB/s"; } }
        /// <summary>
        /// Komprimační poměr vyjadřující velikost pakovanou / velikost zdrojová, například 0.10 (zipuje se na 10% původní velikosti).
        /// Hodnota přednastavená z programu.
        /// </summary>
        public decimal PresetCompressionRatio { get; protected set; }
        /// <summary>
        /// Komprimační poměr vyjadřující velikost pakovanou / velikost zdrojová, například 0.10 (zipuje se na 10% původní velikosti).
        /// Hodnota ověřená praxí, aktuálně načtená z PackPerformance.
        /// </summary>
        public decimal CompressionRatio { get { return PackPerformance.GetCompressionRatio(this.PackMode); } }
        /// <summary>
        /// Text popisující komprimační poměr ve formě úspory místa (97%);
        /// </summary>
        public string CompressionText { get { return (100m * (1m - this.CompressionRatio)).ToString("##0.0").Trim() + " %"; } }
        /// <summary>
        /// Text popisující komprimační poměr ve formě poměru: "1/30"
        /// </summary>
        public string CompressionFraction { get { return _GetFraction(this.CompressionRatio); } }
        /// <summary>
        /// Vrací zlomek, na který se komprimací zmenší vstupní soubory.
        /// Například: "1/30"
        /// </summary>
        /// <param name="ratio"></param>
        /// <returns></returns>
        private string _GetFraction(decimal ratio)
        {
            if (ratio <= 0m) return "-0/0";
            if (ratio >= 1m) return ratio.ToString("##0.0") + "/1"; // = "4/1"
            decimal root = 1 / ratio;                               // Pro ratio = 0.05 => 20;
            
            if (root < 10m) root = 10m;
            // else if (root <= 20m) root = 20m;
            // else if (root <= 50m) root = 50m;
            else if (root < 100m) root = 100m;
            // else if (root <= 200m) root = 200m;
            // else if (root <= 500m) root = 500m;
            else root = 1000m;

            decimal quot = Math.Round(root * ratio, 0);

            return quot.ToString("##0") + "/" + root.ToString("##0");
        }
        public bool MultiThreadSupport { get; protected set; }
        public string ToolTip { get; protected set; }
        #endregion
        #region Kvalifikované odhady
        /// <summary>
        /// Vrátí odhadovaný počet sekund práce
        /// </summary>
        /// <param name="totalInputFileSize"></param>
        /// <returns></returns>
        internal double CalculateEstimateSeconds(long totalInputFileSize)
        {
            double seconds = (double)totalInputFileSize / (double)this.SpeedMBs / 1000000d;          // Počet sekund pro komprimaci daného objemu dat
            return seconds;
        }
        /// <summary>
        /// Vrátí odhadovanou velikost zazipovaných souborů
        /// </summary>
        /// <param name="totalInputFileSize"></param>
        /// <returns></returns>
        internal long CalculateEstimateSize(long totalInputFileSize)
        {
            return (long)((decimal)totalInputFileSize * this.CompressionRatio);
        }
        #endregion
        #region Proces zipování: back thread, abstract metoda Pack()
        /// <summary>
        /// Zahájí proces zipování a ihned vrátí řízení.
        /// S volající stranou komunikuje prostřednictvím handleru 
        /// </summary>
        /// <param name="files"></param>
        /// <param name="outFile"></param>
        /// <param name="callbackHandler"></param>
        internal void Run(IEnumerable<System.IO.FileInfo> files, string outFile, AsyncWorkCallbackHandler callbackHandler)
        {
            this.FilesToPack = files;
            this.OutName = outFile;
            this.CallbackHandler = callbackHandler;

            this.WorkingThread = new System.Threading.Thread(this._RunThread);
            this.WorkingThread.IsBackground = true;
            this.WorkingThread.Name = "pack_" + System.IO.Path.GetFileNameWithoutExtension(outFile);
            this.WorkingThread.Priority = System.Threading.ThreadPriority.Lowest;
            this.WorkingThread.TrySetApartmentState(System.Threading.ApartmentState.MTA);
            
            this.WorkingThread.Start();
        }
        protected virtual void _RunThread()
        {
            try
            {
                this.CallStart();
                this.ResultFile = null;
                this.OutFile = this.Pack();
                if (this.ResultFile == null && !String.IsNullOrEmpty(this.OutFile))
                    this.ResultFile = new System.IO.FileInfo(this.OutFile);
                this.CallSuccess();
            }
            catch (Exception exc)
            {
                this.CallError(exc);
            }
        }
        public virtual void Abort()
        {
            try
            {
                if (this.WorkingThread != null)
                {
                    if (this.WorkingThread.IsAlive)
                    {
                    }
                    this.WorkingThread.Abort();
                }
            }
            catch (Exception exc)
            { }
        }
        protected System.Threading.Thread WorkingThread;
        protected IEnumerable<System.IO.FileInfo> FilesToPack;
        /// <summary>
        /// Název souboru jak by jej chtěla mít volající rutina (= bez přípony)
        /// </summary>
        protected string OutName;
        /// <summary>
        /// Název souboru jak jej deklaruje výkonná metoda (včetně přípony)
        /// </summary>
        protected string OutFile;
        /// <summary>
        /// FileInfo o výsledném souboru
        /// </summary>
        public System.IO.FileInfo ResultFile { get; protected set; }
        /// <summary>
        /// Stav procesu
        /// </summary>
        public AsyncWorkState WorkState { get; protected set; }
        /// <summary>
        /// Výkonná metoda, na třídě PackInfo je abstraktní, výkon provádí konkrétní potomek (podle typu pakování: ZIP, CAB, IONic, ...)
        /// </summary>
        protected abstract string Pack();
        #endregion
        #region Komunikace s volající stranou (Callback)
        private void CallStart()
        {
            this.WorkState = AsyncWorkState.Start;
            if (this.CallbackHandler != null)
                this.CallbackHandler(this, AsyncWorkCallbackArgs.Start());
        }
        protected bool CallProgress(decimal progressRatio)
        {
            this.WorkState = AsyncWorkState.Progress;
            AsyncWorkCallbackArgs args = AsyncWorkCallbackArgs.Progress(progressRatio);
            if (this.CallbackHandler != null)
                this.CallbackHandler(this, args);
            return args.Cancel;
        }
        protected bool CallProgress(string progressMessage)
        {
            this.WorkState = AsyncWorkState.Progress;
            AsyncWorkCallbackArgs args = AsyncWorkCallbackArgs.Progress(progressMessage);
            if (this.CallbackHandler != null)
                this.CallbackHandler(this, args);
            return args.Cancel;
        }
        protected bool CallProgress(decimal progressRatio, string progressMessage)
        {
            this.WorkState = AsyncWorkState.Progress;
            AsyncWorkCallbackArgs args = AsyncWorkCallbackArgs.Progress(progressRatio, progressMessage);
            if (this.CallbackHandler != null)
                this.CallbackHandler(this, args);
            return args.Cancel;
        }
        private void CallSuccess()
        {
            this.WorkState = AsyncWorkState.Success;
            if (this.CallbackHandler != null)
                this.CallbackHandler(this, AsyncWorkCallbackArgs.Success());
        }
        private void CallError(Exception exc)
        {
            this.WorkState = AsyncWorkState.Error;
            if (this.CallbackHandler != null)
                this.CallbackHandler(this, AsyncWorkCallbackArgs.Error(exc));
        }
        protected AsyncWorkCallbackHandler CallbackHandler;
        #endregion
    }
    #endregion
    #region PackIonicInfo : Třída zajišťující komprimaci pomocí algoritmů IONIC.dll.
    /// <summary>
    /// PackIonicInfo : Třída zajišťující komprimaci pomocí algoritmů IONIC.dll.
    /// </summary>
    internal class PackIonicInfo : PackInfo
    {
        protected PackIonicInfo(PackMode packMode, decimal speedMBs, decimal compressionRatio, bool multiThreadSupport, string toolTip)
            : base(packMode, speedMBs, compressionRatio, multiThreadSupport, toolTip)
        { }
        /// <summary>
        /// Vrátí true, pokud tato třída implementuje daný režim
        /// </summary>
        /// <param name="packMode"></param>
        /// <returns></returns>
        internal static bool ImplementPackMode(PackMode packMode)
        {
            return (packMode == PackMode.IonicBestSpeed || packMode == PackMode.IonicBestCompession || packMode == PackMode.IonicBZip2);
        }
        /// <summary>
        /// Vrátí objekt PackIonicInfo pro daný typ komprese
        /// </summary>
        /// <param name="packMode"></param>
        /// <returns></returns>
        internal static new PackInfo Create(PackMode packMode)
        {
            switch (packMode)
            {
                case PackMode.IonicBestSpeed: return new PackIonicInfo(PackMode.IonicBestSpeed, 60m, 0.12m, false, "ZIP file, Ionic library, BestSpeed mode");
                case PackMode.IonicBestCompession: return new PackIonicInfo(PackMode.IonicBestCompession, 35m, 0.09m, false, "ZIP file, Ionic library, BestCompression mode");
                case PackMode.IonicBZip2: return new PackIonicInfo(PackMode.IonicBZip2, 3.2m, 0.04m, false, "ZIP file, Ionic library, BZip2 mode");
            }
            throw new InvalidOperationException("Třída PackIonicInfo nemůže vygenerovat objekt pro režim PackMode = " + packMode.ToString());
        }
        protected override string Pack()
        {
            string zipName = this.OutName + ".zip";
            this._TotalBytes = this.FilesToPack.Sum(fi => fi.Length);
            this._PackedBytes = 0L;
            this._CurrentBytes = 0L;

            Ionic.Zip.CompressionMethod zipMethod = (this.PackMode == Downloader.PackMode.IonicBestCompession || this.PackMode == Downloader.PackMode.IonicBestSpeed ? Ionic.Zip.CompressionMethod.Deflate : (this.PackMode == Downloader.PackMode.IonicBZip2 ? Ionic.Zip.CompressionMethod.BZip2 : Ionic.Zip.CompressionMethod.Deflate));
            Ionic.Zlib.CompressionLevel zipLevel = (this.PackMode == Downloader.PackMode.IonicBestCompession ? Ionic.Zlib.CompressionLevel.BestCompression : (this.PackMode == Downloader.PackMode.IonicBestSpeed ? Ionic.Zlib.CompressionLevel.BestSpeed : (this.PackMode == Downloader.PackMode.IonicBZip2 ? Ionic.Zlib.CompressionLevel.Default : Ionic.Zlib.CompressionLevel.Default)));
            using (Ionic.Zip.ZipFile zipFile = new Ionic.Zip.ZipFile())
            {
                foreach (System.IO.FileInfo fileInfo in this.FilesToPack)
                {
                    string file = fileInfo.FullName;
                    string name = System.IO.Path.GetFileName(file);

                    Ionic.Zip.ZipEntry zipEntry = zipFile.AddFile(file, "");

                    zipEntry.Attributes = fileInfo.Attributes;
                    zipEntry.SetEntryTimes(fileInfo.CreationTime, fileInfo.LastAccessTime, fileInfo.LastWriteTime);

                    zipEntry.CompressionMethod = zipMethod;
                    zipEntry.CompressionLevel = zipLevel;
                }
                zipFile.SaveProgress += new EventHandler<Ionic.Zip.SaveProgressEventArgs>(_ProgressHandler);
                zipFile.Save(zipName);
            }
            return zipName;
        }
        void _ProgressHandler(object sender, Ionic.Zip.SaveProgressEventArgs args)
        {
            switch (args.EventType)
            {
                case Ionic.Zip.ZipProgressEventType.Saving_AfterWriteEntry:
                    // Po uložení souboru (=Entry) si přičtu jeho velikost (z this._CurrentBytes) do sumy this._PackedBytes:
                    this._PackedBytes += this._CurrentBytes;
                    this._CurrentBytes = 0L;
                    break;
                case Ionic.Zip.ZipProgressEventType.Saving_EntryBytesRead:
                    // Pokud jsem se poprvé dozvěděl celkovou velikost souboru, zapamatuji si ji:
                    if (this._CurrentBytes == 0L)
                        this._CurrentBytes = args.TotalBytesToTransfer;
                    long packedBytes = this._PackedBytes + args.BytesTransferred;
                    decimal progressRatio = (decimal)packedBytes / (decimal)this._TotalBytes;
                    this.CallProgress(progressRatio);
                    break;
            }
        }
        private long _TotalBytes;
        private long _PackedBytes;
        private long _CurrentBytes;
    }
    #endregion
    #region PackWinCabInfo : Třída zajišťující komprimaci pomocí algoritmů Windows.Deployment.Cab.dll.
    /// <summary>
    /// PackWinCabInfo : Třída zajišťující komprimaci pomocí algoritmů Windows.Deployment.Cab.dll.
    /// </summary>
    internal class PackWinCabInfo : PackInfo
    {
        protected PackWinCabInfo(PackMode packMode, decimal speedMBs, decimal compressionRatio, bool multiThreadSupport, string toolTip)
            : base(packMode, speedMBs, compressionRatio, multiThreadSupport, toolTip)
        { }
        /// <summary>
        /// Vrátí true, pokud tato třída implementuje daný režim
        /// </summary>
        /// <param name="packMode"></param>
        /// <returns></returns>
        internal static bool ImplementPackMode(PackMode packMode)
        {
            return (packMode == PackMode.WinCabMin || packMode == PackMode.WinCabMax);
        }
        /// <summary>
        /// Vrátí objekt PackWinCabInfo pro daný typ komprese
        /// </summary>
        /// <param name="packMode"></param>
        /// <returns></returns>
        internal static new PackInfo Create(PackMode packMode)
        {
            switch (packMode)
            {
                case PackMode.WinCabMin: return new PackWinCabInfo(PackMode.WinCabMin, 5m, 0.07m, false, "CAB file, Microsoft library, Min compression mode");
                case PackMode.WinCabMax: return new PackWinCabInfo(PackMode.WinCabMax, 2.25m, 0.03m, false, "CAB file, Microsoft library, Max compression mode");
            }
            throw new InvalidOperationException("Třída PackWinCabInfo nemůže vygenerovat objekt pro režim PackMode = " + packMode.ToString());
        }
        protected override string Pack()
        {
            string cabName = this.OutName + ".cab";
            Microsoft.Deployment.Compression.CompressionLevel compressionLevel = (this.PackMode == Downloader.PackMode.WinCabMin ? Microsoft.Deployment.Compression.CompressionLevel.Min : (this.PackMode == Downloader.PackMode.WinCabMax ? Microsoft.Deployment.Compression.CompressionLevel.Max : Microsoft.Deployment.Compression.CompressionLevel.Normal));
            List<string> infi = (new List<System.IO.FileInfo>(this.FilesToPack)).ConvertAll<string>(fi => fi.FullName);
            Microsoft.Deployment.Compression.Cab.CabInfo cabInfo = new Microsoft.Deployment.Compression.Cab.CabInfo(cabName);
            cabInfo.PackFiles(null, infi, null, compressionLevel, this._ProgressHandler);
            return cabName;
        }
        private void _ProgressHandler(object sender, Microsoft.Deployment.Compression.ArchiveProgressEventArgs args)
        {
            decimal progressRatio = (decimal)args.FileBytesProcessed / (decimal)args.TotalFileBytes;
            this.CallProgress(progressRatio);
        }
    }
    #endregion
    #region Pack7zDllInfo : Třída zajišťující komprimaci pomocí algoritmů 7z
    /// <summary>
    /// Pack7zDllInfo : Třída zajišťující komprimaci pomocí algoritmů 7z
    /// </summary>
    internal class Pack7zDllInfo : PackInfo
    {
        protected Pack7zDllInfo(PackMode packMode, decimal speedMBs, decimal compressionRatio, bool multiThreadSupport, string toolTip)
            : base(packMode, speedMBs, compressionRatio, multiThreadSupport, toolTip)
        { }
        /// <summary>
        /// Vrátí true, pokud tato třída implementuje daný režim
        /// </summary>
        /// <param name="packMode"></param>
        /// <returns></returns>
        internal static bool ImplementPackMode(PackMode packMode)
        {
            return (packMode == PackMode.SevenZipSpeed || packMode == PackMode.SevenZipCompression);
        }
        /// <summary>
        /// Vrátí objekt Pack7zDllInfo pro daný typ komprese
        /// </summary>
        /// <param name="packMode"></param>
        /// <returns></returns>
        internal static new PackInfo Create(PackMode packMode)
        {
            switch (packMode)
            {
                case PackMode.SevenZipSpeed: return new Pack7zDllInfo(PackMode.SevenZipSpeed, 5m, 0.07m, false, "7Z file, SevenZip library, Max speed");
                case PackMode.SevenZipCompression: return new Pack7zDllInfo(PackMode.SevenZipCompression, 5m, 0.07m, false, "7Z file, SevenZip library, Max compression");
            }
            throw new InvalidOperationException("Třída Pack7zDllInfo nemůže vygenerovat objekt pro režim PackMode = " + packMode.ToString());
        }
        protected override string Pack()
        {
            string sv7Name = this.OutName + ".7z";
            List<string> infi = (new List<System.IO.FileInfo>(this.FilesToPack)).ConvertAll<string>(fi => fi.FullName);
            string commonRoot = null;
            foreach (string file in infi)
            {
                string path = System.IO.Path.GetDirectoryName(file);
                if (commonRoot == null)
                    commonRoot = path;
                else if (!String.Equals(commonRoot, path, StringComparison.InvariantCultureIgnoreCase))
                {
                    commonRoot = "";
                    break;
                }
            }
            int commonRootLength = (String.IsNullOrEmpty(commonRoot) ? 0 : commonRoot.Length);
            string[] files = infi.ToArray();
            this._LastPercentDone = (byte)0;
            SevenZip.CompressionMethod method = (this.PackMode == Downloader.PackMode.SevenZipSpeed ? SevenZip.CompressionMethod.Deflate : SevenZip.CompressionMethod.Deflate);
            SevenZip.CompressionLevel level = (this.PackMode == Downloader.PackMode.SevenZipSpeed ? SevenZip.CompressionLevel.Fast : SevenZip.CompressionLevel.Ultra);
            SevenZip.SevenZipCompressor szc = new SevenZip.SevenZipCompressor();
            szc.CompressionMethod = method;
            szc.CompressionLevel = level;
            szc.Compressing += new EventHandler<SevenZip.ProgressEventArgs>(_ProgressHandler);
            szc.CompressFiles(sv7Name, commonRootLength, files);
            return sv7Name;
        }

        void _ProgressHandler(object sender, SevenZip.ProgressEventArgs args)
        {
            if (args.PercentDone != this._LastPercentDone)
            {
                decimal progressRatio = (decimal)args.PercentDone / 100m;
                this.CallProgress(progressRatio);
                this._LastPercentDone = args.PercentDone;
            }
        }
        byte _LastPercentDone;
    }
    #endregion
    #region AsyncWorkCallbackHandler, AsyncWorkCallbackArgs, AsyncWorkState
    internal delegate void AsyncWorkCallbackHandler(object caller, AsyncWorkCallbackArgs args);
    internal class AsyncWorkCallbackArgs
    {
        private AsyncWorkCallbackArgs(AsyncWorkState state)
        {
            this.State = state;
        }
        internal static AsyncWorkCallbackArgs Start() { return new AsyncWorkCallbackArgs(AsyncWorkState.Start); }
        internal static AsyncWorkCallbackArgs Progress(decimal progressRatio) { return new AsyncWorkCallbackArgs(AsyncWorkState.Progress) { ProgressRatio = progressRatio }; }
        internal static AsyncWorkCallbackArgs Progress(string progressMessage) { return new AsyncWorkCallbackArgs(AsyncWorkState.Progress) { ProgressMessage = progressMessage }; }
        internal static AsyncWorkCallbackArgs Progress(decimal progressRatio, string message) { return new AsyncWorkCallbackArgs(AsyncWorkState.Progress) { ProgressRatio = progressRatio, ProgressMessage = message }; }
        internal static AsyncWorkCallbackArgs Error(Exception exception) { return new AsyncWorkCallbackArgs(AsyncWorkState.Error) { Exception = exception }; }
        internal static AsyncWorkCallbackArgs Success() { return new AsyncWorkCallbackArgs(AsyncWorkState.Success); }
        internal AsyncWorkState State { get; private set; }
        internal decimal? ProgressRatio { get; private set; }
        internal string ProgressMessage { get; private set; }
        internal bool Cancel { get; set; }
        internal Exception Exception { get; private set; }
    }
    internal enum AsyncWorkState
    {
        None = 0,
        Start,
        Progress,
        Error,
        Success
    }
    #endregion
}
